// 连星WEB版AI通信协议 by clonne/2021
// 使用此协议的引擎均可应用在连星上
// 协议说明：
// 引擎与界面使用文本消息通讯
// 消息的整体格式为：A\nB\nC\r，其中A为消息ID(正整数文本)，B为此消息的类型(文本)，C为附加参数(文本)
// 由\n(换行符 ASCII码10)分隔A/B/C，由\r(回车符 ASCII码13)结尾，A/B/C中不能有\n和\r
// 附加参数可以为多个，由|分隔，每个参数中不能有\n和\r和|
// 协议支持自定义消息类型，但不能与基本消息类型冲突，界面和AI必须完成协议基本消息类型的通讯
//
// 子格式：
// 1 <棋色>         一个字符：b为黑棋 w为白棋
// 2 <坐标点>       三个字符：X-Y
//                      X为[0,14]，表示棋盘的横坐标位置(从左至右)
//                      Y为[0,14]，表示棋盘的纵坐标位置(从上至下)
//
// 以下为由界面向AI发送的基本消息类型(1仅为ID示例)：
// 1\nai.move\nP|S|T|...\r          让AI下一手棋
//              P为AI的所执的<棋色>
//              S为此手棋是否可被交换：
//                  y               可被交换
//                  n               不会被交换
//              T为此手棋的时限(秒)：3 <= T <= 7200,
//              ...为可选参数，由界面与AI引擎自行协定
//              随后AI须按此格式回复：1\n\n<坐标点>\r
//              AI也可以选择不落子(认输)，则按此回复：1\n\npass\r
//
// 1\nai.set_board\nA...\r          更新AI的棋盘状态
//              A...为所有非空位的棋子表示，棋子表示由|分隔
//              棋子表示的格式为：<坐标点>/<棋色>
//
// 1\nai.set_rule\nA\r              更新AI所用的规则
//              A为以下格式之一：
//              forbidden|on        使用禁手
//              forbidden|off       不用禁手
//              !args|...           特殊参数，由界面与AI引擎自行协定
//
// 1\nai.select-a\nP|A...\r         问AI在对方有以下打点的情况下选择哪个点
//              P为打点方的<棋色>
//              A...为这些打点的<坐标点>
//              随后AI须按此格式回复：1\n\nI\r
//              I为所选的点在A...中的线性顺序，从0开始：[0, A...个数)
//
// 1\nai.selects?\nP|N...\r         问AI在N打的情况下打哪几个点
//              P为打点方的<棋色>
//              N...为已打点位置，位置数即为打多少点，对于每个位置
//                  若为<坐标点>，则表示已打点该位置
//                  若为字符'?'，则表示由AI打点该位置
//              随后AI须按此格式回复：1\n\nA...\r
//              A...为这些打点的<坐标点>，A之间由|分隔
//
// 1\nai.swap?\n\r                  问AI是否交换
//              随后AI须按此格式回复：1\n\nP\r
//              P为AI选择的执棋的<棋色>
//
// 1\nai.get_status\n\r             获取AI当前的思考状态
//              AI可以不回应，若回应则按此格式：1\n\nPA...|PB...|T\r
//              PA...和PB...为棋子<坐标点>，表示AI当前思考的点，每个<坐标点>之间由/分隔
//              PA...中的位置为当前重点思考位置(深度)，宜偏少
//              PB...中的位置为当前大范围思考位置(广度)，宜偏多
//              T为AI想说的话，可以为除\n\r|以外的任意字符
//
// 1\nai.reset\n\r                  通知AI重置内部状态
//
// 1\nai.end\n\r                    通知AI关闭引擎
//
//
// 以下为由AI向界面发送的基本消息类型：
//
// ERROR\n\nA\r                     向界面报告错误
//              A为错误消息
//
// SAY\n\nA\r                       向界面报告引擎想说的话
//              A为想说的话
//
// 消息ID是界面与引擎通讯的依据
// 比如界面需要让AI走棋时(黑棋)，先：
//      1\nai.move\nb\r
//      这条消息ID为1，然后AI随后回复同样消息ID的响应：
//      1\n\n7-7\r
// 这是一个标准的需要返回的异步通讯示例
// 本模块封装了消息的编码与解析，并维护了一个消息栈，不需要管理ID
// 同样是以上的效果：
//      import Port from "./ai-port.js"
//      Port.push(Port.ai.move(Port.P_BLACK)).onreply(console.log)
//

import "../../lib/ramda.js"

const Consts = {
    BOARD_SIZE:15,
    BOARD_LEN:225,
    P_EMPTY:'-',
    P_BLACK:'b',
    P_WHITE:'w',
    from_pflag: (pflag)=>(["b","-","w"][1+pflag]),
    STEP_SECOND_MIN: 3,
    STEP_SECOND_MAX: 7200,
    to_i: (x,y)=>(y*Consts.BOARD_SIZE + x),
    to_x: (i)=>(i % Consts.BOARD_SIZE),
    to_y: (i)=>(Math.floor(i / Consts.BOARD_SIZE)),
    STACK_MAX_LEN:16,
}

function Location(from){
    let _i = -1
    const me = {}
    function unpack(text){
        const parts = text.split('-')
        if (parts.length != 2){return}
        const x = Number(parts[0])
        if ((x < 0) || (x >= Consts.BOARD_SIZE)){return}
        const y = Number(parts[1])
        if ((y < 0) || (y >= Consts.BOARD_SIZE)){return}
        _i = Consts.to_i(x, y)
    }
    me.i = (to)=>{
        if (R.is(Number, to)){_i = to}
        return _i
    }
    me.x = (to)=>{
        if (R.is(Number, to)){_i = Consts.to_i(to, Consts.to_y(_i))}
        return Consts.to_x(_i)
    }
    me.y = (to)=>{
        if (R.is(Number, to)){_i = Consts.to_i(Consts.to_x(_i), to)}
        return Consts.to_y(_i)
    }
    me.valid = ()=>{
        return ((_i >= 0) && (_i < Consts.BOARD_LEN))
    }
    me.pack = ()=>{
        return `${Consts.to_x(_i)}-${Consts.to_y(_i)}`
    }
    if (R.is(String, from)){
        unpack(from)
    }else if ((arguments.length == 1) && (R.is(Number, from))){
        _i = from
    }else if (arguments.length == 2){
        _i = Consts.to_i(arguments[0], arguments[1])
    }
    return me
}

function Piece(from){
    let _flag = Consts.P_EMPTY
    const me = {}
    function unpack(text){
        if ((Consts.P_BLACK == text) || (Consts.P_WHITE == text)){
            _flag = text
        }
    }
    me.is_empty = ()=>(Consts.P_EMPTY == _flag)
    me.is_black = ()=>(Consts.P_BLACK == _flag)
    me.is_white = ()=>(Consts.P_WHITE == _flag)
    me.pack = ()=>{
        return _flag
    }
    if (R.is(String, from)){unpack(from)}
    return me
}

function Board(from){
    const _pieces = []
    function reset(){
        _pieces.splice(0, _pieces.length)
        for (let i = 0; i < Consts.BOARD_LEN; i++){
            _pieces.push(Piece())
        }
    }
    function unpack(pcodes){
        if (pcodes.length > 0){
            pcodes.forEach((pcode)=>{
                const parts = pcode.split('/')
                if (parts.length != 2){return}
                const loc = Location(parts[0])
                if (! loc.valid()){return}
                _pieces[Consts.to_i(loc.x(), loc.y())] = Piece(parts[1])
            })
        }
    }
    const me = {}
    me.set = (x, y, p)=>{
        if (R.is(String, p)){
            _pieces[Consts.to_i(x, y)] = Piece(p)
        }else{
            _pieces[Consts.to_i(x, y)] = p
        }
        return me
    }
    me.for_pieces = (f)=>{
        for (let i = 0; i < _pieces.length; i++){
            f(_pieces[i], Consts.to_x(i), Consts.to_y(i))
        }
    }
    me.pack = ()=>{
        const _ps = []
        me.for_pieces((p, x, y)=>{
            if (! p.is_empty()){
                _ps.push(`${Location(x,y).pack()}/${p.pack()}`)
            }
        })
        return _ps.join('|')
    }
    reset()
    if (R.is(String, from)){unpack(from.split('|'))}
    else if (R.is(Array, from)){unpack(from)}
    return me
}

function Sobject(code){
    let _callbacks = {
        "reply":R.always(true),
    }
    const me = {...code}
    me.onreply = (f)=>{
        _callbacks["reply"] = f
        return me
    }
    me.callback = (args)=>{
        for (let k in _callbacks){
            _callbacks[k](...(args))
        }
        return me
    }
    return me
}
const Stack = {
    _objids:[],
    _objs:{},
    _remove_first(){
        delete Stack._objs[Stack._objids[0]]
        Stack._objids.splice(1)
        return Stack
    },
    _swapid(a, b){
        if (a != b){
            const _a = Stack._objids[a]
            Stack._objids[a] = Stack._objids[b]
            Stack._objids[b] = _a
        }
        return Stack
    },
    push(code){
        const obj = Sobject(code)
        Stack._objids.push(code.id)
        Stack._objs[code.id] = obj
        if (Stack._objids.length >= Consts.STACK_MAX_LEN){
            Stack._remove_first()
        }
        return obj
    },
    pop(code){
        const obj = Stack._objs[code.id]
        if (R.isNil(obj)){return code}
        obj.callback(code.args)
        for (let i = 0; i < Stack._objids.length; i++){
            if (Stack._objids[i] == code.id){
                Stack._swapid(i, 0)._remove_first()
                return {id:NaN, target:false}
            }
        }
        return code
    },
    clear(){
        Stack._objids = []
        Stack._objs = {}
        return Stack
    }
}

function make_uid(){
    return (Date.now()).toString()
}
function encode_args(args){
    const _args = R.isNil(args) ? [] : (R.is(Array, args) ? args : [args.toString()])
    if (_args.length > 1){
        return _args.map((a)=>(a.toString())).join('|')
    }else if (_args.length == 1){
        return _args[0]
    }
    return ""
}
function encode(target, args){
    const args_msg = encode_args(args)
    const id = make_uid()
    const msg = `${id}\n${target}\n${args_msg}\r`
    return {id, target, args, msg, toString:()=>msg}
}
function decode(msg){
    const parts = msg.split('\n')
    if (parts.length == 3){
        const id = parts[0]
        const target = parts[1]
        if (parts[2].endsWith('\r')){
            const args = parts[2].slice(0, parts[2].length-1).split('|')
            return {id, target, args}
        }
    }
    return {id:NaN, target:false}
}

function reply(id, args){
    const args_msg = encode_args(args)
    return `${id}\n\n${args_msg}\r`
}

export default {
    ...Consts,
    Location,
    Piece,
    Board,
    push:Stack.push, pop:Stack.pop, clear:Stack.clear,
    encode, decode, reply,
}
